# coding=utf8
from datetime import datetime
import queue
import threading
import time

import serial

from .str_predicate import *


class WaitLogTimedOutError(Exception):
    def __init__(self, message):
        super().__init__(message)


def _try_decode(data):
    try:
        return data.decode("utf8")
    except UnicodeDecodeError:
        try:
            return data.decode("gbk")
        except UnicodeDecodeError:
            return str(data)[1:]


class _QhIpcSerialOutputObserver(object):
    def __init__(self):
        self.output_queue = queue.Queue()

    def put(self, log_line):
        self.output_queue.put(log_line)

    def iget(self, timeout=None):
        while True:
            yield self.output_queue.get(block=True, timeout=timeout)

    def empty(self):
        self.output_queue.empty()


class QhIpcSerial(object):

    def __init__(self, port, baudrate=115200, username="root", password="iot_ipc@360", redirect_logfiles=None,
                 debug_printer: callable = None, if_need_login=True):
        super(QhIpcSerial, self).__init__()
        self.serial: serial.Serial = serial.Serial(port, baudrate=baudrate, timeout=60)
        self.username = username
        self.password = password
        self._output_observers = []

        if redirect_logfiles is None:
            redirect_logfiles = []
        self.redirect_logfiles = redirect_logfiles

        self._output_read_thread = threading.Thread(target=self._output_producer)
        self._output_read_thread.start()
        self._debug_printer = debug_printer
        if self._debug_printer:
            self._debug_printer("init")
            self._debug_print_outputs()
        try:
            if if_need_login:
                self.check_and_relogin()
        except Exception as e:
            print(e)
            self.serial.close()
            raise e
        self.redirect_logs()

    def exec(self, command, wait=0.25, active_shell_before=True):
        if type(command) == bytes:
            self.serial.write(command)
        else:
            if active_shell_before:
                self.serial.write(b"\r")
            self.serial.flushInput()
            self.serial.write(command.encode("utf8"))
            self.serial.write(b"\r")
        # self.serial.flushInput()
        time.sleep(wait)

    def unregister_output_observer(self, observer):
        # copy on write
        new_list = self._output_observers[:]
        new_list.remove(observer)
        self._output_observers = new_list

    def register_output_observer(self, observer):
        # copy on write
        new_list = self._output_observers[:]
        new_list.append(observer)
        self._output_observers = new_list

    def _output_producer(self):
        while self.serial.is_open:
            try:
                lines = self.serial.iread_until(expected=b"\r\n")
                for line in lines:
                    if not self.serial.is_open:
                        break
                    line = line.replace(b"\r\n", b"")
                    try:
                        line = _try_decode(line)
                        if line is None:
                            continue
                        for observer in self._output_observers:
                            observer.put(line)
                    except UnicodeDecodeError:
                        continue
            except:
                pass

    def _debug_print_outputs(self):
        def _print_outputs():
            observer = _QhIpcSerialOutputObserver()
            self.register_output_observer(observer)
            try:
                while self.serial.is_open:
                    try:
                        for line in observer.iget(timeout=2):
                            self._debug_printer('【' + str(datetime.now()) + '】' + line)
                    except queue.Empty:
                        continue
            finally:
                self.unregister_output_observer(observer)

        t = threading.Thread(target=_print_outputs)
        t.start()

    def send_break(self):
        self.exec("\r")

    def close(self):
        self.serial.close()

    def redirect_logs(self):
        self.stop_logs_redirect()
        tails = []
        if self.redirect_logfiles is None or len(self.redirect_logfiles) == 0:
            return
        for log in self.redirect_logfiles:
            self.exec(f"touch {log}")
            tails.append(f"tail -f {log} &")
        self.exec(" ".join(tails))

    def clear_redirect_logs(self):
        for log in self.redirect_logfiles:
            self.exec(f"> {log}")

    def stop_logs_redirect(self):
        self.exec("killall tail")

    def check_and_relogin(self, timeout=15):
        for i in range(5):
            if self._check_and_relogin(timeout=timeout):
                return

        raise Exception("login failed")

    def _check_and_relogin(self, timeout=10):
        # 激活登录console，20秒为等待可能的bootload
        is_logged_in = oneof(
                contains(self.username+"@"),
                contains("]#"),
                contains("]"),
                contains("#"),
                contains("~ #"),
                contains("~#"),
                endwith(']# '),
                endwith('#'),
                endwith(']'),
                endwith('# '),
            )

        # 不同型号 登录/不登录 的字段不一样，暂时都重新登录，
        # log = self.exec_and_wait_log("\r\n", oneof(
        #     contains("login:"),
        #     is_logged_in
        # ), timeout=1, repeat=20)
        # if is_logged_in(log):
        #     # 已登陆
        #     return True
        self.exec('\r')
        self.exec(self.username, wait=1)
        self.exec(self.password, wait=1, active_shell_before=False)
        result = self.exec_and_wait_log("\r", oneof(
            endwith("login:"),
            is_logged_in,
            contains("Login incorrect"),
            contains("(none) login:"),
            contains("login"),
            contains("Login"),
        ), repeat=5, timeout=timeout)
        return is_logged_in(result)

    def observe_outputs(self):
        return QhIpcSerialOutputObserverContext(self)

    def exec_and_wait_log(self, command, predicate: callable, timeout: int = None, repeat: int = 0):
        for i in range(repeat + 1):
            try:
                return self._exec_and_wait_log(command, predicate, timeout=timeout)
            except WaitLogTimedOutError:
                # 最后一次，抛出
                if i == repeat:
                    raise
                continue

    def _exec_and_wait_log(self, command, predicate: callable, timeout: int = None):
        observer = _QhIpcSerialOutputObserver()
        self.register_output_observer(observer)
        if command is not None:
            self.exec(command)
        start_time = time.monotonic()
        try:
            for line in observer.iget(timeout=timeout):
                if timeout is not None and 0 < timeout < time.monotonic() - start_time:
                    raise WaitLogTimedOutError("wait log timed out")
                if predicate(line):
                    return line
        except queue.Empty:
            raise WaitLogTimedOutError("wait log timed out")
        finally:
            self.unregister_output_observer(observer)
        raise WaitLogTimedOutError("no matched line found")

    def wait_log(self, predicate: callable, timeout: int = None):
        return self.exec_and_wait_log(None, predicate, timeout=timeout)

    def save_directory_as_single_file(self, directory: str, dest: str, timedout=20):
        """ 保存设备目录内所有文件内容至一个文件 """
        command = f"tail -n +1 {directory}/*"

        return self._exec_and_save_outputs(command, dest, timedout=timedout)

    def save_file(self, target, dest=None, timedout=20):

        if dest is None:
            dest = target.split("/")[-1]

        command = f"cat {target}"
        return self._exec_and_save_outputs(command, dest, timedout=timedout)

    def _exec_and_save_outputs(self, command: str, dest: str, timedout=20):
        self.stop_logs_redirect()
        old_timeout = self.serial.timeout
        self.serial.timeout = timedout
        try:
            with self.observe_outputs() as o:
                self.exec(
                    f"{command} ; echo \"__finished__\"")
                self.serial.flushInput()
                with open(dest, "w") as f:
                    start_flag = False
                    for line in o.iget(timeout=timedout):
                        if not start_flag and contains(command)(line):
                            start_flag = True
                            continue
                        if contains("__finished__")(line):
                            return dest
                        if start_flag:
                            f.write(line + "\n")
                            f.flush()
        finally:
            self.serial.timeout = old_timeout
            self.redirect_logs()


class QhIpcSerialOutputObserverContext(object):

    def __init__(self, qh_ipc_serial: QhIpcSerial):
        super(QhIpcSerialOutputObserverContext, self).__init__()
        self._serial = qh_ipc_serial
        self._observer = None

    def __enter__(self):
        self._observer = _QhIpcSerialOutputObserver()
        self._serial.register_output_observer(self._observer)
        return self._observer

    def __exit__(self, type, value, traceback):
        self._serial.unregister_output_observer(self._observer)
